Java Servlet 学习之环境配置
Servlet 是什么?
Java Servlet 是运行在 Web 服务器或应用服务器上的程序,它是作为来自 Web 浏览器或其他 HTTP 客户端的请求和 HTTP 服务器上的数据库或应用程序之间的中间层。
Java Servlet 是运行在带有支持 Java Servlet 规范的解释器 的 web 服务器(Tomcat)上的 Java 类。
注意:安装 Tomcat 这部分就是看那篇笔记,这里就不再重复赘述了~
Servlet 可以使用 javax.servlet
和 javax.servlet.http
包创建,它是 Java 企业版的标准组成部分,Java 企业版是支持大型开发项目的 Java 类库的扩展版本。
这些类实现 Java Servlet 和 JSP 规范(Java Server Pages)。在写本教程的时候,二者相应的版本分别是 Java Servlet 2.5 和 JSP 2.1。
Java Servlet 就像任何其他的 Java 类一样已经被创建和编译。在您安装 Servlet 包并把它们添加到您的计算机上的 Classpath 类路径中之后,您就可以通过 JDK 的 Java 编译器或任何其他编译器来编译 Servlet。
Servlet、Server 和 Service 的区别
Servlet(Server Applet)称为小服务程序或服务连接器,用 Java 编写的服务器端程序,具有独立于平台和协议的特性(说白了就是使用 Java提供的 Web API 编写的小程序)
当针对一个 JSP的第一个请求到来时,该页面转化为对应于 JSP中的指令的 JAVA类。容器负责创建对象,实际上就是一个 Servlet。
Server 表示 web 服务器,常规意义的服务器就是指它
Service 就是使用 HTTP、XML 或 JSON 相互通信的一种方式,不需要任何人参与。(就是表示用于交换数据的一个过程,并不是实体)
Servlet 架构
下图显示了 Servlet 在 Web 应用程序中的位置。
在JavaEE平台上,处理 TCP 连接,解析 HTTP 协议这些底层工作统统扔给现成的 Web服务器去做,我们只需要把自己的应用程序跑在 Web服务器上。为了实现这一目的,JavaEE提供了 Servlet API,我们使用 Servlet API 编写自己的 Servlet 来处理 HTTP 请求,Web服务器实现 Servlet API 接口,实现底层功能:
┌───────────┐
│My Servlet │
├───────────┤
│Servlet API│
┌───────┐ HTTP ├───────────┤
│Browser│<──────>│Web Server │
└───────┘ └───────────┘
编写一个最简单的 Servlet
项目结构
整个工程结构如下:
web-servlet-hello
├── pom.xml
└── src
└── main
├── java
│ └── com
│ └── itranswarp
│ └── learnjava
│ └── servlet
│ └── HelloServlet.java
├── resources
└── webapp
└── WEB-INF
└── web.xml
添加 API 包
Servlet API 是一个 jar 包,我们需要通过 Maven 来引入它,才能正常编译。
编写 pom.xml
文件如下:
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itranswarp.learnjava</groupId>
<artifactId>web-servlet-hello</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<java.version>11</java.version>
</properties>
<dependencies>
<!-- 注意,得 4.0 版本才能直接使用注解 -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<!-- 这里可以设置打包的名字,默认就是使用这个做根路径的 -->
<build>
<finalName>studyjavaservlet</finalName>
</build>
</project>
注意到这个 pom.xml
与普通 Java程序有个区别,打包类型不是 jar,而是 war,表示 Java Web Application Archive:
<packaging>war</packaging>
引入的 Servlet API 如下:
注意到 <scope>
指定为 provided,表示编译时使用,但不会打包到 .war
文件中,因为运行期 Web 服务器本身已经提供了 Servlet API 相关的 jar 包。
我们还需要在工程目录下创建一个 web.xml
描述文件,放到 src/main/webapp/WEB-INF
目录下(固定目录结构,不要修改路径,注意大小写)。
文件内容可以固定如下:
<!DOCTYPE web-app PUBLIC
"-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"
"http://java.sun.com/dtd/web-app_2_3.dtd">
<web-app>
<display-name>Archetype Created Web Application</display-name>
</web-app>
编写 Servlet
一个 Servlet 总是继承自 HttpServlet,然后覆写 doGet()
或 doPost()
方法。注意到 doGet()
方法传入了 HttpServletRequest 和 HttpServletResponse 两个对象,分别代表 HTTP请求和响应。
我们使用 Servlet API 时,并不直接与底层 TCP 交互,也不需要解析 HTTP 协议,因为 HttpServletRequest 和 HttpServletResponse 就已经封装好了请求和响应。
以发送响应为例,我们只需要设置正确的响应类型,然后获取 PrintWriter,写入响应即可。
如下代码所示:
// WebServlet注解表示这是一个Servlet,并映射到地址/:
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 设置响应类型:
resp.setContentType("text/html");
// 获取输出流:
PrintWriter pw = resp.getWriter();
// 写入响应:
pw.write("<h1>Hello, world!</h1>");
// 最后不要忘记flush强制输出:
pw.flush();
}
}
@WebServlet 注解的使用
使用 @WebServlet
注解,可以简化繁琐的 xml 配置。使用注解前的 web.xml
配置如下:
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<servlet>
<servlet-name>helloServlet</servlet-name>
<servlet-class>coderead.servlet.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>helloServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
改为使用注解以后的 web.xml
配置如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
</web-app>
编译后在容器中运行
运行 Maven 命令 mvn clean package
,在 target 目录下得到一个 hello.war
文件,这个文件就是我们编译打包后的 Web应用程序。
现在问题又来了:我们应该如何运行这个 war 文件?
普通的 Java程序是通过启动 JVM,然后执行 main()
方法开始运行。但是 Web应用程序有所不同,我们无法直接运行 war 文件,必须先启动 Web服务器,再由 Web服务器加载我们编写的 HelloServlet,这样就可以让 HelloServlet 处理浏览器发送的请求。
因此,我们首先要找一个支持 Servlet API 的 Web 服务器。常用的服务器有:
无论使用哪个服务器,只要它支持 Servlet API 4.0(因为我们引入的 Servlet 版本是4.0),我们的 war 包都可以在上面运行。
这里我们选择使用最广泛的开源免费的 Tomcat 服务器。
把 hello.war
复制到 Tomcat 的 webapps 目录下,然后切换到 bin 目录,执行 startup.sh
或 startup.bat
启动 Tomcat 服务器:
访问测试(这里使用子域名)
为啥路径是 /studyjavaservlet/
而不是 /?
因为一个 Web服务器允许同时运行多个 Web App,而我们的 Web App 叫 studyjavaservlet,因此,第一级目录 /studyjavaservlet/
表示 Web App 的名字,后面的 /
才是我们在 HelloServlet 中映射的路径。(上面那个 ROOT 才是 /
)
那能不能直接使用 /
而不是 /studyjavaservlet/?
毕竟 /
比较简洁。
答案是肯定的。先关闭 Tomcat(执行 shutdown.sh
或 shutdown.bat
),然后删除 Tomcat 的 webapps 目录下的所有文件夹和文件,最后把我们的 studyjavaservlet.war
复制过来,改名为 ROOT.war
,文件名为 ROOT 的应用程序将作为默认应用,启动后直接访问 http://localhost:8080/
即可。
容器的原理
实际上,类似 Tomcat 这样的服务器也是 Java 编写的,启动 Tomcat 服务器实际上是启动 Java 虚拟机,执行 Tomcat 的 main()
方法,然后由 Tomcat 负责加载 .war
文件,并创建一个 HelloServlet 实例,最后以多线程的模式来处理 HTTP 请求。
如果 Tomcat 服务器收到的请求路径是 /
(假定部署文件为 ROOT.war
),就转发到 HelloServlet 并传入 HttpServletRequest 和 HttpServletResponse 两个对象。
因为我们编写的 Servlet 并不是直接运行,而是由 Web 服务器加载后创建实例运行,所以,类似 Tomcat 这样的 Web 服务器也称为 Servlet 容器。
在 Servlet 容器中运行的 Servlet 具有如下特点:
- 无法在代码中直接通过
new
创建 Servlet 实例,必须由 Servlet 容器自动创建Servlet 实例; - Servlet 容器只会给每个 Servlet 类创建唯一实例;
- Servlet 容器会使用多线程执行
doGet()
或doPost()
方法。
所以复习一下 Java多线程的内容,我们可以得出结论:
- 在 Servlet 中定义的实例变量会被多个线程同时访问,要注意线程安全;
- HttpServletRequest 和 HttpServletResponse 实例是由 Servlet 容器传入的局部变量,它们只能被当前线程访问,不存在多个线程访问的问题;
- 在
doGet()
或doPost()
方法中,如果使用了 ThreadLocal,但没有清理,那么它的状态很可能会影响到下次的某个请求,因为 Servlet 容器很可能用线程池实现线程复用。
使用嵌入式 Tomcat 开发
上面那种手动丢到 Tomcat的 webapp 目录的方式实在太麻烦了,而且它调试还需要打开 Tomcat 的远程调试端口并且连接上去。
- 启动 JVM并执行 Tomcat 的
main()
方法; - 加载 war 并初始化 Servlet;
- 正常服务。
启动 Tomcat 无非就是设置好 classpath 并执行 Tomcat 某个 jar 包的 main()
方法,我们完全可以把 Tomcat 的 jar 包全部引入进来,然后自己编写一个 main()
方法,先启动 Tomcat,然后让它加载我们的 webapp 就行。
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.itranswarp.learnjava</groupId>
<artifactId>web-servlet-embedded</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<java.version>11</java.version>
<tomcat.version>9.0.26</tomcat.version>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>${tomcat.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
引入依赖 tomcat-embed-core 和 tomcat-embed-jasper,引入的 Tomcat 版本 <tomcat.version>
为9.0.26。
不必引入 Servlet API,因为引入 Tomcat 依赖后自动引入了 Servlet API。因此,我们可以正常编写 Servlet 如下:
@WebServlet(urlPatterns = "/")
public class HelloServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.setContentType("text/html");
String name = req.getParameter("name");
if (name == null) {
name = "world";
}
PrintWriter pw = resp.getWriter();
pw.write("<h1>Hello, " + name + "!</h1>");
pw.flush();
}
}
然后,我们编写一个 main()
方法,启动Tomcat服务器:
public class Main {
public static void main(String[] args) throws Exception {
// 启动 Tomcat:
Tomcat tomcat = new Tomcat();
tomcat.setPort(Integer.getInteger("port", 8080));
tomcat.getConnector();
// 创建 webapp:
Context ctx = tomcat.addWebapp("", new File("src/main/webapp").getAbsolutePath());
WebResourceRoot resources = new StandardRoot(ctx);
resources.addPreResources(
new DirResourceSet(resources, "/WEB-INF/classes", new File("target/classes").getAbsolutePath(), "/"));
ctx.setResources(resources);
tomcat.start();
tomcat.getServer().await();
}
}
这样,我们直接运行 main()
方法,即可启动嵌入式 Tomcat 服务器,然后,通过预设的 tomcat.addWebapp("", new File("src/main/webapp")
,Tomcat 会自动加载当前工程作为根 webapp,可直接在浏览器访问 http://localhost:8080/
Reference
参考资料 廖雪峰的 Web基础 参考资料 菜鸟教程的 Java Servlet 教程 参考资料 Difference between servlet and web service